package vip.xiaomoli.idempotent.aop;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.stereotype.Component;
import vip.xiaomoli.idempotent.annotation.IdempotentTransaction;
import vip.xiaomoli.idempotent.component.RequestIDContainer;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@Aspect
@Component
public class IdempotentAspect {

    @Resource
    private DataSource dataSource;

    @Around("@annotation(idemAnn)")
    public Object around(ProceedingJoinPoint joinPoint, IdempotentTransaction idemAnn) throws Throwable {
        Object returnValue;
        String requestID = RequestIDContainer.get();
        Connection conn = null;
        try {
            conn = DataSourceUtils.getConnection(dataSource);
            if (isRepeatRequest(conn, requestID)) {
                returnValue = getReturnValue(joinPoint, conn, requestID);
            } else {
                returnValue = joinPoint.proceed();
                if (null != returnValue) recordReturnValue(returnValue, conn, requestID);
            }
        } finally {
            DataSourceUtils.releaseConnection(conn, dataSource);
            RequestIDContainer.remove();
        }
        return returnValue;
    }

    private boolean isRepeatRequest(Connection conn, String requestID) throws SQLException {
        PreparedStatement pstmt = null;
        try {
            String sql = "INSERT IGNORE INTO idempotent(requestID,addTime) VALUES(?,now())";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, requestID);
            return 0 == pstmt.executeUpdate();
        } finally {
            JdbcUtils.closeStatement(pstmt);
        }
    }

    private Object getReturnValue(ProceedingJoinPoint joinPoint, Connection conn, String requestID) throws SQLException, JsonProcessingException {
        Object returnValue = null;
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Type returnType = methodSignature.getMethod().getGenericReturnType();
        if (!"void".equals(returnType.getTypeName())) {
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                String sql = "SELECT returnValue FROM idempotent WHERE requestID=?";
                pstmt = conn.prepareStatement(sql);
                pstmt.setString(1, requestID);
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    String returnStr = rs.getString(1);
                    returnValue = json2Object(returnStr, returnType);
                }
            } finally {
                JdbcUtils.closeResultSet(rs);
                JdbcUtils.closeStatement(pstmt);
            }
        }
        return returnValue;
    }

    private void recordReturnValue(Object returnValue, Connection conn, String requestID) throws JsonProcessingException, SQLException {
        ObjectMapper mapper = new ObjectMapper();
        String returnStr = mapper.writeValueAsString(returnValue);
        PreparedStatement pstmt = null;
        try {
            String sql = "UPDATE idempotent SET returnValue=? WHERE requestID=?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, returnStr);
            pstmt.setString(2, requestID);
            pstmt.executeUpdate();
        } finally {
            JdbcUtils.closeStatement(pstmt);
        }
    }

    private Object json2Object(String jsonStr, Type returnType) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        JavaType javaType = mapper.constructType(returnType);
        return mapper.readValue(jsonStr, javaType);
    }

}
